<template>
	<div class="plugins">
		<div class="plugins__header">
			<cl-select-button v-model="tab.active" :options="tab.list" @change="tab.onChange" />

			<el-input
				class="search"
				v-model="plugin.keyWord"
				:placeholder="$t('搜索插件名称')"
				clearable
				@change="
					plugin.refresh({
						page: 1
					})
				"
			>
				<template #prefix>
					<cl-svg name="search" :size="16" />
				</template>
			</el-input>
		</div>

		<div class="plugins__container" v-loading="plugin.loading">
			<el-scrollbar :ref="setRefs('scrollbar')" @scroll="plugin.onScroll">
				<div
					class="list"
					:class="{
						'is-installed': isInstalled
					}"
					@dragover="onDragover"
					@drop="onDrop"
					@mousemove="onMousemove"
				>
					<el-row :gutter="10">
						<el-col :xs="24" :sm="12" :md="8" :lg="6" v-if="isInstalled">
							<div class="item">
								<cl-upload :before-upload="onBeforeUpload" accept=".cool">
									<div class="item-inner is-add">
										<cl-svg name="plus" />
									</div>
								</cl-upload>
							</div>
						</el-col>

						<el-col
							v-for="(item, index) in plugin[tab.active]"
							:key="index"
							:xs="24"
							:sm="12"
							:md="8"
							:lg="6"
						>
							<div class="item">
								<div class="item-inner">
									<div class="op" v-if="isInstalled && !item.isVue">
										<div
											class="cl-comm__icon is-delete"
											@click="plugin.remove(item, index)"
										>
											<cl-svg name="delete" />
										</div>

										<div
											class="cl-comm__icon is-set"
											@click="config.open(item)"
										>
											<cl-svg name="set" />
										</div>
									</div>

									<div class="c">
										<cl-image
											class="mr-[15px]"
											:radius="6"
											:src="item.logo"
											:size="40"
										/>

										<div class="det">
											<div class="tag">
												<el-tag
													disable-transitions
													size="small"
													effect="dark"
													v-if="item.name"
												>
													{{ item.name }}
												</el-tag>
												<el-tag
													disable-transitions
													size="small"
													effect="plain"
												>
													v{{ item.version || '1.0.0' }}
												</el-tag>
												<el-tag
													disable-transitions
													size="small"
													effect="plain"
													:type="item.isVue ? 'success' : 'info'"
													v-if="isInstalled"
												>
													{{ item.isVue ? 'Vue' : $t('后端') }}
												</el-tag>
											</div>

											<p class="title line-clamp-1">
												{{ item.label || $t('未知') }}
											</p>

											<p class="desc line-clamp-2">
												{{ item.description || $t('暂无描述') }}
											</p>

											<div class="author">
												<span>{{ item.author || 'Ta' }}：</span>
												<span>{{ item.updateTime || '2025-01-01' }}</span>
											</div>
										</div>
									</div>

									<div class="f">
										<cl-switch
											v-model="item.status"
											@change="plugin.onEnable(item)"
											v-if="
												isInstalled &&
												(item.isVue ? item.enable !== undefined : true)
											"
										/>

										<span class="mr-auto"></span>

										<el-button
											type="success"
											@click="demo.open(item)"
											v-if="item.demo"
										>
											{{ $t('示例') }}
										</el-button>

										<el-button
											type="success"
											@click="pic.preview(item.pics)"
											v-if="!isEmpty(item.pics)"
										>
											{{ $t('预览') }}
										</el-button>

										<el-button
											@click="plugin.doc(item)"
											v-if="item.doc || item.readme"
										>
											{{ $t('文档') }}
										</el-button>
									</div>
								</div>
							</div>
						</el-col>
					</el-row>
				</div>

				<el-empty :image-size="80" v-if="isEmpty(plugin[tab.active])" />
			</el-scrollbar>
		</div>

		<!-- 示例代码 -->
		<cl-dialog v-model="demo.visible" :title="demo.title" width="60%">
			<el-tabs v-model="demo.active" type="card">
				<el-tab-pane
					v-for="(item, index) in demo.tabs"
					:key="index"
					:label="item.name"
					:name="index"
					:lazy="index != 0"
				>
					<component :is="item.component" />
				</el-tab-pane>
			</el-tabs>
		</cl-dialog>

		<!-- 图片预览 -->
		<cl-dialog
			v-model="pic.visible"
			:title="$t('图片预览')"
			width="80%"
			padding="0"
			hide-header
			transparent
		>
			<div class="plugins__pics">
				<div class="close" @click="pic.close">
					<cl-svg name="close" />
				</div>

				<div class="list">
					<div
						class="item"
						v-for="(item, index) in pic.list"
						:key="index"
						@click="pic.open(index)"
					>
						<img :src="item" />
					</div>
				</div>

				<div class="op">
					<div class="prev" @click="pic.prev">
						<cl-svg name="left" />
					</div>

					<div class="next" @click="pic.next">
						<cl-svg name="right" />
					</div>
				</div>
			</div>

			<el-image-viewer
				:url-list="pic.list"
				infinite
				:initial-index="pic.index"
				teleported
				@close="pic.onClose"
				v-if="pic.visible && pic.show"
			/>
		</cl-dialog>

		<!-- 详情预览 -->
		<cl-editor-preview
			:ref="setRefs('editorPreview')"
			name="wang"
			:show-btn="false"
			:title="`${plugin.data?.name}（v${plugin.data?.version}）${$t('说明文档')}`"
		>
			<template #prepend>
				<div class="flex align-center mb-[10px] select-none">
					<span class="mr-[20px]">{{ $t('作者') }}：{{ plugin.data?.author }}</span>
					<span>{{ $t('更新时间') }}：{{ plugin.data?.updateTime }}</span>
				</div>
			</template>
		</cl-editor-preview>

		<!-- 插件配置 -->
		<cl-form :ref="setRefs('config')">
			<template #slot-tools>
				<div class="flex align-center">
					<el-button type="success" @click="config.formatter">{{
						$t('格式化')
					}}</el-button>
					<cl-upload-space
						:show-list="false"
						:text="$t('插入文件链接')"
						@confirm="config.onFileConfirm"
					/>
				</div>
			</template>
		</cl-form>
	</div>
</template>

<script lang="ts" setup>
defineOptions({
	name: 'helper-plugins-vue'
});

import { reactive, nextTick, markRaw, onMounted, computed } from 'vue';
import { useCool, module } from '/@/cool';
import {
	assign,
	debounce,
	isEmpty,
	isFunction,
	isObject,
	isString,
	merge,
	orderBy,
	startsWith
} from 'lodash-es';
import { ElMessage, ElMessageBox } from 'element-plus';
import { marked } from 'marked';
import { useI18n } from 'vue-i18n';

interface Plugin {
	name?: string;
	label?: string;
	version?: string;
	author?: string;
	logo?: string;
	doc?: string;
	readme?: string;
	demo?: string;
	isVue?: boolean;
	status?: number;
	config: {
		[key: string]: any;
	};
	[key: string]: any;
}

const { router, service, refs, setRefs } = useCool();
const helper = module.get('helper');
const { t } = useI18n();

// 选项卡
const tab = reactive({
	active: 'installed',
	p: '',

	list: [
		{
			label: t('已安装'),
			value: 'installed'
		},
		{
			label: t('全部插件'),
			value: 'shop'
		},
		{
			label: t('插件开发'),
			value: 'dev'
		}
	],

	change(val: string) {
		tab.active = val;
	},

	onChange(val: string) {
		if (val == 'dev') {
			tab.active = tab.p || 'installed';

			nextTick(() => {
				window.open('https://cool-js.com/plugin/create');
			});
		} else {
			tab.p = val;
			plugin.refresh();
		}
	}
});

// 是否已安装插件
const isInstalled = computed(() => tab.active == 'installed');

// 插件
const plugin = reactive({
	// 已安装
	installed: [] as Plugin[],
	// 插件市场
	shop: [] as Plugin[],
	// 详情
	data: null as Plugin | null,

	// 初始化
	init() {
		plugin.refresh();
	},

	// 安装
	install(file: File) {
		const next = (force: boolean) => {
			const data = new FormData();

			data.append('files', file);
			data.append('force', String(force));

			service.plugin.info
				.request({
					url: '/install',
					method: 'POST',
					data,
					headers: {
						'Content-Type': 'multipart/form-data'
					}
				})
				.then(res => {
					if (!res) {
						ElMessage.success(t('插件安装成功'));
						tab.change('installed');
						plugin.refresh();

						return;
					}

					if (res.type == 0) {
						ElMessageBox.confirm(res.message, t('提示'), {
							type: 'error',
							showConfirmButton: false
						})
							.then(() => {
								next(true);
							})
							.catch(() => null);
					}

					if (res.type == 1 || res.type == 2) {
						ElMessageBox.confirm(res.message, t('提示'), {
							type: 'warning',
							confirmButtonText: t('继续')
						})
							.then(() => {
								next(true);
							})
							.catch(() => null);
					}

					if (res.type == 3) {
						next(true);
					}
				})
				.catch(err => {
					ElMessage.error(err.message);
				});
		};

		next(false);
	},

	// 卸载
	remove(item: Eps.PluginInfoEntity, index: number) {
		ElMessageBox.confirm(t(`确定要卸载插件【{name}】吗？`, { name: item.name }), t('提示'), {
			type: 'warning'
		})
			.then(() => {
				service.plugin.info
					.delete({
						ids: [item.id]
					})
					.then(() => {
						plugin.installed.splice(index, 1);
						ElMessage.success(t('卸载成功'));
					})
					.catch(err => {
						ElMessage.error(err.message);
					});
			})
			.catch(() => null);
	},

	// 文档
	async doc(item: Plugin) {
		if (item.readme) {
			plugin.data = item;
			refs.editorPreview.open(await marked.parse(item.readme!));
		} else if (item.doc) {
			window.open(item.doc);
		}
	},

	// 启用/禁用
	onEnable(item: Plugin) {
		if (item.isVue) {
			service.request({
				url: '/__cool_updatePlugin',
				method: 'POST',
				data: {
					name: item.name,
					data: item.config,
					enable: item.status == 1
				},
				proxy: false
			});
		} else {
			service.plugin.info
				.update({
					id: item.id,
					status: item.status ? 1 : 0
				})
				.then(() => {
					ElMessage.success(item.status ? t('启用成功') : t('禁用成功'));
				})
				.catch(err => {
					ElMessage.error(err.message);
				});
		}
	},

	// 分页列表
	keyWord: '',

	params: {
		page: 1,
		size: 24,
		// type: 68,
		isMy: false
	},

	loaded: false,
	loading: false,

	async refresh(params?: any) {
		if (tab.active == 'shop') {
			assign(plugin.params, params || {});

			const data = {
				keyWord: plugin.keyWord || undefined,
				...plugin.params
			};

			if ((plugin.loaded || plugin.loading) && data.page != 1) {
				return;
			}

			plugin.loading = true;

			await service
				.request({
					url: `${helper.options.api}/app/plugin/info/page`,
					method: 'POST',
					data
				})
				.then((res: ClCrud.Response['page']) => {
					const list = res.list.map(e => {
						return {
							...e,
							label: e.name,
							name: '',
							logo: e.avatarUrl,
							author: e.nickName,
							doc: `https://cool-js.com/plugin/${e.id}`
						};
					});

					if (data.page == 1) {
						plugin.shop = list;
					} else {
						plugin.shop.push(...list);
					}

					plugin.loaded = plugin.shop.length >= res.pagination.total;
					plugin.params.page = res.pagination.page;
				})
				.catch(err => {
					ElMessage.error(err.message);
				});

			plugin.loading = false;
		} else {
			service.plugin.info.page({ size: 1000 }).then(res => {
				plugin.installed = res.list
					.concat(
						orderBy(
							module.list.filter(e => e.type == 'plugins' && e.label),
							'order',
							'desc'
						)
					)
					.map(e => {
						if (e.author == 'COOL') {
							e.logo = 'https://cool-js.com/logo.png';
						}

						const isVue = e.type == 'plugins';

						if (isVue) {
							return {
								...e,
								label: e.label || e.name,
								status: e.enable === false ? 0 : 1,
								config: e.options || {},
								children: e.views,
								isVue
							};
						} else {
							return {
								...e,
								logo: 'data:image/jpg;base64,' + e.logo,
								label: e.name!,
								name: e.keyName,
								config: e.config || {}
							};
						}
					})
					.filter(e => {
						return plugin.keyWord
							? e.label
									.toLocaleLowerCase()
									.includes(plugin.keyWord.toLocaleLowerCase())
							: true;
					});
			});
		}
	},

	onScroll: debounce(({ scrollTop }: { scrollTop: number }) => {
		const { clientHeight, scrollHeight } = refs.scrollbar.wrapRef;

		if (scrollTop + clientHeight >= scrollHeight) {
			if (tab.active == 'shop') {
				plugin.refresh({
					page: plugin.params.page + 1
				});
			}
		}
	}, 100)
});

// 插件配置
const config = reactive({
	insert(content: string) {
		const input = refs.configInput.ref;

		// 获取当前光标
		const cursor = input.selectionStart;

		// 插入文案
		input.value = input.value.slice(0, cursor) + (content || '') + input.value.slice(cursor);
	},

	formatter() {
		try {
			refs.config?.setForm(
				'config',
				JSON.stringify(JSON.parse(refs.config.form.config), null, 4)
			);

			return true;
		} catch (err) {
			ElMessage.error(t('参数格式错误') + '：' + err);
			return false;
		}
	},

	onFileConfirm(list: Eps.SpaceInfoEntity[]) {
		const urls = list.map(e => e.url);
		const content = urls.length > 1 ? JSON.stringify(urls) : urls[0];

		config.insert(content!);
	},

	open(item: Plugin) {
		refs.config?.open({
			title: t('设置'),

			form: {
				id: item.id,
				config: item.config
			},

			items: [
				{
					label: t('参数'),
					prop: 'config',
					component: {
						name: 'el-input',
						props: {
							type: 'textarea',
							rows: 15
						},
						ref: setRefs('configInput')
					}
				},
				{
					component: {
						name: 'slot-tools'
					}
				}
			],

			on: {
				open(data) {
					if (!data.config) {
						data.config = {};
					}

					if (isString(data.config)) {
						try {
							data.config = JSON.parse(data.config);
						} catch (err) {
							data.config = {};
						}
					}

					if (isObject(data.config)) {
						data.config = JSON.stringify(data.config);
					}

					config.formatter();
				},
				submit(data, { done, close }) {
					if (!config.formatter()) {
						done();
						return;
					}

					service.plugin.info
						.update(data)
						.then(() => {
							ElMessage.success(t('修改成功'));
							merge(item, data);
							close();
						})
						.catch(err => {
							ElMessage.error(err.message);
							done();
						});
				}
			}
		} as ClForm.Options);
	},

	close() {
		refs.config?.close();
	}
});

// 插件示例
const demo = reactive({
	visible: false,
	title: '',
	active: 0,
	tabs: [] as any[],

	async open(item: Plugin) {
		if (isString(item.demo)) {
			if (startsWith(item.demo, 'http')) {
				window.open(item.demo);
			} else {
				router.push(item.demo);
			}
		} else {
			demo.visible = true;
			demo.active = 0;
			demo.tabs = [];
			demo.title = item.label!;

			demo.tabs = await Promise.all(
				(item.demo || []).map(async (e: any) => {
					if (e) {
						const c = await (isFunction(e.component) ? e.component() : e.component);

						return {
							...e,
							component: markRaw(c.default)
						};
					}
				})
			);
		}
	},

	close() {
		demo.visible = false;
	}
});

// 图片预览
const pic = reactive({
	visible: false,
	show: false,
	index: 0,
	list: [] as string[],

	preview(urls: string[]) {
		pic.visible = true;
		pic.list = urls;
	},

	open(num: number) {
		pic.show = true;
		pic.index = num;
	},

	close() {
		pic.visible = false;
	},

	onClose() {
		pic.show = false;
	},

	prev() {
		document.querySelector('.plugins__pics .list')?.scrollBy({
			left: -500,
			behavior: 'smooth'
		});
	},

	next() {
		document.querySelector('.plugins__pics .list')?.scrollBy({
			left: 500,
			behavior: 'smooth'
		});
	}
});

// 上传
function onBeforeUpload(file: File) {
	plugin.install(file);
	return false;
}

// 拖拽
function onDragover(e: DragEvent) {
	e.preventDefault();
}

// 放下
function onDrop(e: DragEvent) {
	e.preventDefault();

	if (e.dataTransfer) {
		const file = e.dataTransfer.files[0];

		ElMessageBox.confirm(t('检测到插件，是否安装'), t('提示'), {
			type: 'warning',
			confirmButtonText: t('安装')
		})
			.then(() => {
				plugin.install(file);
			})
			.catch(() => null);
	}
}

// 鼠标移动
function onMousemove(e: MouseEvent) {
	if (!isInstalled.value) {
		return false;
	}

	const list = document.querySelectorAll('.plugins .list .item');

	list.forEach(item => {
		const rect = item.getBoundingClientRect();
		const x = e.clientX - rect.left - 250;
		const y = e.clientY - rect.top - 250;

		if (e.type !== 'scroll') {
			requestAnimationFrame(() => {
				(item as HTMLElement).style.setProperty('--x', `${x}px`);
				(item as HTMLElement).style.setProperty('--y', `${y}px`);
			});
		}
	});
}

onMounted(() => {
	plugin.init();
});
</script>

<style lang="scss" scoped>
.plugins {
	height: 100%;
	background-color: var(--el-bg-color);

	&__header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 10px;

		.search {
			width: 240px;

			:deep(.el-input__wrapper) {
				background-color: var(--el-fill-color-light);
				box-shadow: none;
				border-radius: 8px;
				height: 39px;
			}
		}
	}

	&__container {
		height: calc(100% - 59px);

		.list {
			padding: 0 10px;

			.item {
				border-radius: 10px;
				margin-bottom: 10px;
				height: 200px;
				width: 100%;
				box-sizing: border-box;
				cursor: pointer;
				position: relative;
				overflow: hidden;
				border: 1px solid var(--el-border-color-light);

				&::before {
					content: '';
					position: absolute;
					inset: 0;
					height: 500px;
					width: 500px;
					background: radial-gradient(
						closest-side circle,
						var(--el-text-color-regular),
						transparent
					);
					transform: translate(var(--x, 1000px), var(--y, 1000px));
				}

				&-inner {
					position: absolute;
					inset: 1px;
					border-radius: 8px;
					background-color: var(--el-bg-color);

					&.is-add {
						display: flex;
						align-items: center;
						justify-content: center;

						.cl-svg {
							font-size: 40px;
							color: var(--el-text-color-placeholder);
						}
					}

					.op {
						display: flex;
						position: absolute;
						top: 12px;
						right: 12px;
						z-index: 2;

						.cl-comm__icon {
							margin-left: 5px;

							&.is-delete {
								&:hover {
									.cl-svg {
										color: var(--el-color-danger);
									}
								}
							}
						}
					}

					.c {
						display: flex;
						box-sizing: border-box;
						padding: 15px;
						position: relative;
						height: calc(100% - 43px);

						.set {
							position: absolute;
							right: 10px;
							top: 10px;
							font-size: 18px;
							color: var(--el-color-info);
						}

						.det {
							display: flex;
							flex-direction: column;
							flex: 1;

							.tag {
								display: flex;
								flex-wrap: wrap;
								margin-bottom: 10px;

								.el-tag {
									margin-right: 5px;
								}

								.el-skeleton__item {
									height: 20px;
									width: 50px;
									margin-right: 5px;
									border-radius: 4px;
								}
							}

							.title {
								font-size: 14px;
								font-weight: bold;
								margin-bottom: 5px;
							}

							.desc {
								font-size: 12px;
								color: var(--el-text-color-regular);
								margin-bottom: 14px;
								flex: 1;
							}

							.author {
								font-size: 12px;
								color: var(--el-text-color-secondary);
							}
						}
					}

					.f {
						display: flex;
						align-items: center;
						justify-content: space-between;
						padding: 0 15px;
						height: 28px;

						.el-button {
							font-size: 12px;
							height: 28px;
							margin-left: 8px;
							border-radius: 6px;
						}
					}
				}
			}

			&:not(.is-installed) {
				.item {
					transition: border-color 0.3s;

					&:hover {
						border-color: var(--el-color-primary);
					}
				}
			}
		}
	}

	&__pics {
		padding: 10px;
		position: relative;

		.close {
			position: absolute;
			right: 20px;
			top: 20px;
			border: 1px solid var(--el-border-color-light);

			.cl-svg {
				font-size: 20px;
			}
		}

		.list {
			display: flex;
			overflow: auto hidden;
			user-select: none;
			border-radius: 10px;

			&::-webkit-scrollbar {
				display: none;
			}

			.item {
				display: flex;
				align-items: center;
				position: relative;
				transition: transform 0.3s;
				cursor: pointer;
				margin: 0 15px 0 0;
				flex-shrink: 0;

				img {
					height: 60vh;
					border-radius: 12px;
				}

				&:hover {
					img {
						border-color: var(--el-border-color-dark);
					}
				}
			}
		}

		.op {
			display: flex;
			justify-content: flex-end;
			padding: 20px 0 0 0;

			.prev,
			.next {
				margin: 0 10px;
			}
		}

		.close,
		.op .prev,
		.op .next {
			height: 36px;
			width: 36px;
			border-radius: 36px;
			background-color: var(--el-bg-color);
			cursor: pointer;
			z-index: 2;
			display: flex;
			align-items: center;
			justify-content: center;
			font-size: 16px;
			transition: all 0.2s;

			.cl-svg {
				color: var(--el-text-color-regular);
			}

			&:hover {
				transform: scale(1.1);

				.cl-svg {
					color: var(--el-text-color-primary);
				}
			}
		}
	}
}
</style>
